//
// Created by Lenovo on 2023/3/10.
//

/* 字符串函数和内存函数 */

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include <ctype.h>

int my_strlen1(const char *str)
{
    int count = 0;
    assert(str);
    while (*str != '\0')
    {
        count++;
        str++;
    }
    return count;
}

int my_strlen2(const char *str)
{
    assert(str);
    if (*str != '\0')
    {
        return 1 + my_strlen2(str + 1);
    }
    return 0;
}

int my_strlen3(const char *str)
{
    char *tmp = (char *) str;
    while (*tmp != '\0')
    {
        tmp++;
    }
    return (int) (tmp - str);
}

char *my_strcpy(char *dest, const char *src)
{
    char *ret = dest;
    assert(dest && src);
    while ((*dest++ = *src++))
    {
        ;
    }
    return ret;
}

char *my_strcat(char *dest, const char *src) // const 指的是不能通过 src 改变它所指向的空间内容，但是可以通过别的变量改变
{
    char *ret = dest;
    assert(dest && src);
    // 1.找到目标空间的 '\0'
    while (*dest != '\0')
    {
        dest++;
    }
    // 2.拷贝字符串 - strcpy
    my_strcpy(dest, src);
    return ret;
}

int my_strcmp(const char *str1, const char *str2)
{
    assert(str1 && str2);
    while (*str1 == *str2)
    {
        if (*str1 == '\0')
        {
            return 0;
        }
        str1++;
        str2++;
    }
    if (*str1 > *str2)
    {
        return 1;
    }
    else
    {
        return -1;
    }
    /* return *str1 - *str2; */
}

char *my_strstr(const char *str1, const char *str2)
{
    assert(str1 && str2);
    //两个临时变量
    char *s1 = NULL;
    char *s2 = NULL;

    while (*str1)
    {
        s1 = (char *) str1;
        s2 = (char *) str2;
        while (*str1 != *str2)
        {
            str1++;
        }
        while (*s1 && *s2 && *s1 == *s2) // s1，s2 不能为 '\0'
        {
            s1++;
            s2++;
        }
        if (*s2 == '\0')
        {
            return (char *) str1;
        }
    }
    return NULL;
}

void* my_memcpy(void *dest, const void *src, size_t num) //单位是字节
{
    void *ret = dest;
    assert(dest && src);
    //设计思路：返回目标空间的起始地址
    /*
    for (int i = 0; i < num; i++)
    {
        *(char *) (dest + i) = *(char *) (src + i);
    }
    return dest;
    */
    while (num--)
    {
        *(char *) dest = *(char *) src;
        dest = (char *) dest + 1;
        src = (char *) src + 1;
    }
    return ret;
}

void* my_memmove(void *dest, const void *src, size_t num)
{
    void *ret = dest;
    assert(dest && src);
    // dest 落在 src 前面的时候，需要从前向后拷贝
    // dest 落在 src 后面的时候，需要从后向前拷贝
    if (dest < src)
    {
        //前 -> 后
        while (num--)
        {
            *(char *) dest = *(char *) src;
            dest = (char *) dest + 1;
            src = (char *) src + 1;
        }
    }
    else
    {
        //后 -> 前
        while (num--)
        {
            *(char *) (dest + num) = *(char *) (src + num);
        }
    }
    return ret;
}

int main()
{
    // 1.strlen - 求字符串长度
    {
        char arr[] = "abc\0def";
        char arr0[] = {'a', 'b', 'c'};
        int len = strlen(arr);
        int len0 = strlen(arr0);
        printf("%d\n", len);// 3
        printf("%d\n", len0);//随机值，说明字符串内必须有 '\0' 才能求出长度
    }
    //返回 '\0' 之前的字符个数

    {
        if (strlen("abc") - strlen("abcdef") > 0)
        {
            printf(">\n");// >
        }
        else
        {
            printf("<=\n");
        }
    }
    //返回类型是 size_t(无符号整型)，说明没有负数(类似于 sizeof)，3 - 6 = -3 写成无符号整型就是一个很大的正数

    //模拟实现 strlen
    {
        char arr[] = "bit";
        int len1 = my_strlen1(arr);// 1.计数器
        int len2 = my_strlen2(arr);// 2.递归 - 不创建临时变量
        int len3 = my_strlen3(arr);// 3.指针减指针
        printf("%d %d %d\n", len1, len2, len3);
    }

    // 2.strcpy - 字符串拷贝
    {
        char arr1[] = "abcdef";
        char arr2[] = { 0 };
        strcpy(arr2, arr1);// strcpy(char *destination, const char *source)
        printf("%s\n", arr2);

        char arr3[20] = { 0 };//不能直接赋值
        /* arr3 = arr1; */ // error:Array type 'char[20]' is not assignable
    }
    {
        char arr1[] = "abc\0def";
        char arr2[] = { 0 };
        strcpy(arr2, arr1);
        printf("%s\n", arr2);// abc
        //拷贝：遇到 '\0' 停止，并且把 '\0' 拷贝过去，'\0' 后面的值不再拷贝
        //原字符串中必须含有 '\0'，才能正确拷贝
    }
    {
        char arr1[20] = "abcdefghi";
        char arr2[3] = { 0 };
        /* strcpy(arr2, arr1); */ //程序会崩溃，所以目标字符串长度要大于原字符串
    }
    {
        char *p = "abcdefghi";
        char arr2[20] = "hehe";
        /* strcpy(p, arr2); */ //报错：原字符串的空间必须可以被修改，但是常量字符串不能被修改
        printf("%s\n", p);
    }
    //模拟实现 strcpy
    {
        char arr1[] = "hehe";
        char arr2[20] = { 0 };
        my_strcpy(arr2, arr1);
        printf("%s\n", arr2);
        printf("%s\n", my_strcpy(arr2, arr1));
    }

    // 3.strcat - 追加，把源字符串的内容追加到目标字符串 '\0' 的后面
    {
        char arr1[20] = "hello ";
        char arr2[] = "world";
        strcat(arr1, arr2);// strcat(char *dest, const char *src)
        printf("%s\n", arr1);
        //经测试，可知追加会把目标字符串的 '\0' 覆盖掉，追加到源字符串的 '\0'，而源字符串的 '\0' 也会被追加过去
        //同理，目标字符串和源字符串必须含有 '\0'，目标空间必须足够大，并且可修改
    }
    //模拟实现 strcat
    {
        char arr1[20] = "hello ";
        char arr2[] = "world";
        my_strcat(arr1, arr2);
        printf("%s\n", arr1);
    }
    {
        //自己追加自己，可行吗？
        char arr1[20] = "bit";
        /* my_strcat(arr1, arr1); */
        printf("%s\n", arr1);
        //不可行，因为在 strcpy 的时候，dest 指向 arr1 末尾的 '\0'，src 指向 arr1 开头的 'b'
        //追加的时候，dest 和 src 同时 ++，它们之间总是差了 strlen(arr1)
        //原来的停止条件是 *src == '\0'，但是源字符串已经被 dest 改变，'\0' 已经不存在了，所以程序会死循环
        /*  b   i   t  \0
         *  ^          ^
         * src ------ dest  --------------------------> 死循环
         * 变为 bitbitbitbit...
         */
    }

    // 4.strcmp - 比较对应位置上字符的大小
    {
        char arr1[] = "abcdef";
        char arr2[] = "bbq";
        int ret = strcmp(arr1, arr2);//当第一个字符串某个字母对应 ASCII 码值小于第二个字符串对应位置的 ASCII 码值，就返回小于 0 的值，等于就返回 0，大于就返回大于 0 的值
        printf("%d\n", ret);// -1
    }
    //模拟实现 strcmp
    {
        char arr1[] = "abzpw";
        char arr2[] = "abq";
        int ret = my_strcmp(arr1, arr2);
        printf("%d\n", ret);// 1
    }

    //上面讲的字符串操作函数都是长度不受限制的字符串函数，源字符串有几个字符就会操作几个字符，像 VS 这样的编译器会认为这样的函数不安全(没有越界检查，可能会造成越界访问)
    //下面的字符串操作函数都是长度受限制的字符串函数，多了一个参数：操作的字符个数
    // 1.strncpy
    {
        char arr1[] = "abcdef";
        char arr2[20] = "xxxx";
        strncpy(arr2, arr1, 3);
        printf("%s\n", arr2);// abcx
        strncpy(arr2, arr1, 8);
        printf("%s\n", arr2);// abcdef
        //虽然源字符串只有 7 个字符，但是要求复制 8 个，那么缺的部分被补充 '\0'
    }

    // 2.strncat
    {
        char arr1[20] = "hello \0xxxxxxxxxxx";
        char arr2[] = "abcdef";
        strncat(arr1, arr2, 3);
        printf("%s\n", arr1);// hello abc\0  说明 strncat 会在追加的后面补一个 \0
        //所以这个函数是可以自己给自己追加的
        strncat(arr1, arr2, 8);
        printf("%s\n", arr1);// hello abcabcdef\0xxxx  说明 strncat 在源字符串字符个数不足操作 size 的时候不会补充 '\0'
    }

    // 3.strncmp
    {
        char arr1[] = "abcdef";
        char arr2[] = "abcq";
        int ret = strncmp(arr1, arr2, 4);
        printf("%d\n", ret);// -13
    }

    // ----------------------------------------------------------------------------- 分隔线
    // 1.strstr - char *strstr(const char *str1, const char *str2)
    //在 str1 中寻找 str2，如果找到了就返回 str2 第一次出现的(开头字符的)地址，如果没找到返回空指针
    {
        char arr1[] = "abcdebcdf";
        char arr2[] = "bcd";
        char *p = strstr(arr1, arr2);
        if (p)
        {
            printf("%s\n", p);//bcdebcdf
        }
        else
        {
            printf("找不到\n");
        }
    }
    // char *strchr(const char *src, int character) - 在 str(字符串) 中找 chr(字符) 第一次出现的地址
    // strrchr - 同理，找最后一个 chr 出现的地址

    //模拟实现 strstr
    {
        char arr1[] = "abcdebcdf";
        char arr2[] = "bcd";
        char *p = my_strstr(arr1, arr2);
        if (p)
        {
            printf("%s\n", p);//bcdebcdf
        }
        else
        {
            printf("找不到\n");
        }
    }

    // 2.strtok - char *strtok(char *str, const char *sep)
    //假设有一个邮箱 zpengwei@yeah.net，那么整个邮箱被 @ . 分割为三个部分
    // @ . 分隔符
    // sep 参数是个字符串，定义了用作分隔符的集合(把所有分隔符放到一起)
    //第一个参数指定一个字符串，它包含了 0 个或者多个由 sep 字符串中一个或者多个分隔符分隔的标记
    // strtok 函数找到 str 中的下一个标记，并将其用 '\0' 结尾，返回一个指向这个标记的指针
    //注：strtok 函数会改变被操作的字符串，所以在使用 strtok 函数切分的字符串一般都是临时拷贝的内容并且可修改
    // strtok 函数的第一个参数不为 NULL ，函数将找到 str 中第一个标记，strtok 函数将保存它在字符串中的位置
    // strtok 函数的第一个参数为 NULL ，函数将在同一个字符串中被保存的位置开始，查找下一个标记
    //如果字符串中不存在更多的标记，则返回 NULL 指针

    {
        char arr[] = "zpengwei@yeah.net";
        char buf[30] = { 0 };
        strcpy(buf, arr);//副本，备份数据

        const char *p = "@.";
        char *str = strtok(buf, p);
        printf("%s\n", str);

        str = strtok(NULL, p);
        printf("%s\n", str);

        str = strtok(NULL, p);
        printf("%s\n", str);
        //更改的字符串 zpengwei\0yeah\0net\0 ...

        str = strtok(NULL, p);
        /* printf("%s\n", str); */ //找不到，返回空指针
    }
    //优化
    {
        char arr[] = "zpengwei@yeah.net";
        char buf[30] = { 0 };
        strcpy(buf, arr);//副本，备份数据

        const char *p = "@.";
        char *str = NULL;
        for (str = strtok(buf, p); str != NULL; str = strtok(NULL, p))
        {
            printf("%s\n", str);
        }
        /* const char *buf = "zpengwei@yeah.net"
         * strtok(buf, p); */ //常量字符串不能更改，会报错
    }

    // 3.strerror - char *strerror(int num)
    //返回错误码，所对应的错误信息
    {
        char *p = strerror(0);
        printf("%s\n", p);// No error

        p = strerror(1);
        printf("%s\n", p);// Operation not permitted

        p = strerror(2);
        printf("%s\n", p);// No such file or directory

        p = strerror(3);
        printf("%s\n", p);// No such process
    }
    // C语言的库函数在调用失败的时候，会将一个错误码存放在一个叫 errno 的变量中，如果我们想知道发生了什么错误信息，就可以将 errno 中的错误码翻译成错误信息

    //例 - 读写文件
    {
        //打开文件
        //打开文件的时候，如果文件的打开方式是 "r"，文件存在则打开成功，文件不存在打开失败，打开失败返回 NULL
        FILE *pf = fopen("test.txt", "r");
        if (pf == NULL)
        {
            perror("打开文件失败");// void perror(char *)   printf + strerror
            // (置顶) 打开文件失败: No such file or directory
            printf("打开文件失败，原因是：%s\n", strerror(errno));//头文件
        }
        //读写文件
        // ...
        //关闭文件
        fclose(pf);
        pf = NULL;

    }

    // ----------------------------------------------------------------------------- 分隔线
    //前面讲的函数都是和字符串相关的，但是我们在操作数据的时候，不仅仅是操作字符串的数据
    // 1.memcpy - void* memcpy(void *destination, void *source, size_t num)
    // 把从 source 开始向后的 num 个字节的数据拷贝到 destination 中，返回目标空间的起始地址
    // void* - 通用类型的指针，可以接受任意类型数据的地址
    // memcpy 函数的设计者，不知道未来程序员使用 memcpy 拷贝的是什么类型的数据
    {
        int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int arr2[8] = { 0 };
        memcpy(arr2, arr1, 20);// 20 个字节
    }
    {
        float arr1[] = {1.0F, 2.0F, 3.0F, 4.0F, 5.0F};
        float arr2[8] = { 0 };
        memcpy(arr2, arr1, 12);//前三个数据
    }
    //模拟实现 memcpy
    {
        int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int arr2[8] = { 0 };
        my_memcpy(arr2, arr1, 20);
        for (int i = 0; i < 8; i++)
        {
            printf("%d ", arr2[i]);// 1 2 3 4 5 0 0 0
        }
        printf("\n");
    }
    {
        int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        my_memcpy(arr1 + 2, arr1, 20);//把 1 2 3 4 5 拷贝到 3 4 5 6 7
        for (int i = 0; i < 10; i++)
        {
            printf("%d ", arr1[i]);// 1 2 1 2 1 2 1 8 9 10
        }
        printf("\n");
    }
    //所以我们发现：在内存重叠的时候，使用 memcpy 可能出现意想不到的结果
    //这个时候，推荐使用 memmove 函数

    // 2.memmove - void* memmove(void *destination, void *source, size_t num)
    //和 memcpy 的差别就是 memmove 函数处理的源内存块和目标内存块是可以重叠的
    {
        int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        memmove(arr1 + 2, arr1, 20);//把 1 2 3 4 5 拷贝到 3 4 5 6 7
        for (int i = 0; i < 10; i++)
        {
            printf("%d ", arr1[i]);// 1 2 1 2 3 4 5 8 9 10
        }
        printf("\n");
    }
    //模拟实现 memmove
    {
        int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        my_memmove(arr1 + 2, arr1, 20);//把 1 2 3 4 5 拷贝到 3 4 5 6 7
        for (int i = 0; i < 10; i++)
        {
            printf("%d ", arr1[i]);// 1 2 1 2 3 4 5 8 9 10
        }
        printf("\n");
    }
    //部分编译器(VS，CLion)的 memcpy 经过优化，也可以拷贝重叠的数据
    // memmove >= memcpy

    // 3.memcpy - int memcpy(const void *ptr1, const void *ptr2, size_t num)
    // 返回值 >0 =0 <0
    {
        int arr1[] = {1, 2, 3, 4, 5};
        int arr2[] = {1, 2, 3, 4, 6};
        int ret1 = memcmp(arr1, arr2, 16);// 0
        int ret2 = memcmp(arr1, arr2, 17);// -1
        printf("%d %d\n", ret1, ret2);
    }

    // 4.memset - void* memset()
    //内存设置函数
    {
        int arr[10] = { 0 };
        memset(arr, 1, sizeof(arr));//单位是字节
        for (int i = 0; i < 10; i++)
        {
            printf("%p ", arr[i]);// 01010101 ...
        }
        printf("\n");
    }

    //字符分类函数，如果它的参数符合下列条件就返回真，头文件 <ctype.h>
    // iscntrl         任何控制字符
    // isspace         空白字符：空格' '，换页'\f'，换行'\n'，回车'\r'，制表符'\t'或者垂直制表符'\v'
    // isdigit         十进制数字 0 ~ 9
    // isxdigit        十六进制数字，包括所有十进制数字，小写字母 a ~ f，大写字母 A ~ F
    // islower         小写字母 a ~ z
    // isupper         大写字母 A ~ Z
    // isalpha         字母 a ~ z 或 A ~ Z
    // isalnum         字母或者数字，a ~ z，A ~ Z，0 ~ 9
    // ispunct         标点符号，任何不属于数字或者字母的圆形字符(可打印)
    // isgraph         任何图形字符
    // isprint         任何可打印字符，包括图形字符和空白字符

    {
        printf("%d\n", isdigit('6'));// 4
        printf("%d\n", isspace('2'));// 0
        printf("%d\n", islower('x'));// 1
    }

    //字符转换
    // tolower - 变小写 - int tolower(int c)
    // toupper - 变大写
    {
        char arr[128] = { 0 };
        printf("请输入字符串:>");
        gets(arr);
        int i = 0;
        while (arr[i])
        {
            if (isupper(arr[i]))
            {
                arr[i] = tolower(arr[i]);
            }
            printf("%c", arr[i]);
            i++;
        }
        printf("\n");
    }

    return 0;
}






















